package com.zhou.combine.security;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.zhou.combine.security.entity.Menu;
import com.zhou.combine.security.mapper.MenuMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.security.access.method.MethodSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.util.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * MenuMappingHandler
 *
 * @author zhouxianfeng
 * @date 2021-1-7
 */
@Component
public class MenuMappingHandler extends WebApplicationObjectSupport implements EmbeddedValueResolverAware,InitializingBean {

    private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";

    @Value("${spring.application.name}")
    private String applicationName;

    @Nullable
    private StringValueResolver embeddedValueResolver;

    @Autowired
    public MethodSecurityMetadataSource methodSecurityMetadataSource;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public void afterPropertiesSet() {
        initHandlerMethods();
        Menu menu=new Menu();
        menu.setDeleteFlag(false);
        menu.setModule(applicationName);
        List<Menu> list = menuMapper.selectList(new LambdaQueryWrapper<Menu>(menu));
        this.mappingRegistry.registry.forEach((k,v)->{
            Set<String> directPaths = k.getPatternsCondition().getPatterns();
            Set<RequestMethod> methods = k.getMethodsCondition().getMethods();
            Method method = v.getHandlerMethod().getMethod();
            Class<?> beanType = v.getHandlerMethod().getBeanType();
            //Collection<ConfigAttribute> attributes = methodSecurityMetadataSource.getAttributes(method, beanType);
            if (directPaths.size()>0){
                Menu newMenu=new Menu();
                newMenu.setMethod(StringUtils.join(methods, ","));
                newMenu.setPath(directPaths.iterator().next());
                newMenu.setModule(applicationName);
                LambdaQueryWrapper<Menu> queryWrapper=new LambdaQueryWrapper(newMenu);
                Menu one = menuMapper.selectOne(queryWrapper);
                if (one==null){
                    newMenu.setDeleteFlag(false);
                    menuMapper.insert(newMenu);
                }
                if (one!=null&&one.getDeleteFlag()){
                    one.setDeleteFlag(false);
                    menuMapper.updateById(one);
                }else {
                    list.remove(one);
                }
            }
        });
        if (!list.isEmpty()){
            Collection<Long> collect = list.stream().map(t -> t.getId()).collect(Collectors.toList());
            Menu newMenu=new Menu();
            newMenu.setDeleteFlag(true);
            LambdaUpdateWrapper<Menu> queryWrapper=new LambdaUpdateWrapper();
            queryWrapper.in(Menu::getId,collect);
            menuMapper.update(newMenu,queryWrapper);
        }

        //this.mappingRegistry
    }
    private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
            new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));

    private static class EmptyHandler {
        @SuppressWarnings("unused")
        public void handle() {
            throw new UnsupportedOperationException("Not implemented");
        }
    }

    String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler";

    public RequestMappingInfo lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        List<MenuMappingHandler.Match> matches = new ArrayList<>();
        List<RequestMappingInfo> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
        if (directPathMatches != null) {
            addMatchingMappings(directPathMatches, matches, request);
        }
        if (matches.isEmpty()) {
            addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
        }
        if (!matches.isEmpty()) {
            MenuMappingHandler.Match bestMatch = matches.get(0);
            if (matches.size() > 1) {
                Comparator<MenuMappingHandler.Match> comparator = new MenuMappingHandler.MatchComparator(getMappingComparator(request));
                matches.sort(comparator);
                bestMatch = matches.get(0);
                if (logger.isTraceEnabled()) {
                    logger.trace(matches.size() + " matching mappings: " + matches);
                }
                if (CorsUtils.isPreFlightRequest(request)) {
                    //return PREFLIGHT_AMBIGUOUS_MATCH;
                    return null;
                }
                MenuMappingHandler.Match secondBestMatch = matches.get(1);
                if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                    Method m1 = bestMatch.handlerMethod.getMethod();
                    Method m2 = secondBestMatch.handlerMethod.getMethod();
                    String uri = request.getRequestURI();
                    throw new IllegalStateException(
                            "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
                }
            }
            request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
            handleMatch(bestMatch.mapping, lookupPath, request);
            return bestMatch.mapping;
        }
        else {
            return null;
        }
    }

    protected void handleMatch(RequestMappingInfo mapping, String lookupPath, HttpServletRequest request) {
        request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
    }

    protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
        return (info1, info2) -> info1.compareTo(info2, request);
    }

    private void addMatchingMappings(Collection<RequestMappingInfo> mappings, List<MenuMappingHandler.Match> matches, HttpServletRequest request) {
        for (RequestMappingInfo mapping : mappings) {
            RequestMappingInfo match = getMatchingMapping(mapping, request);
            if (match != null) {
                matches.add(new MenuMappingHandler.Match(match,
                        this.mappingRegistry.getRegistrations().get(mapping).getHandlerMethod()));
            }
        }
    }

    private class MatchComparator implements Comparator<MenuMappingHandler.Match> {

        private final Comparator<RequestMappingInfo> comparator;

        public MatchComparator(Comparator<RequestMappingInfo> comparator) {
            this.comparator = comparator;
        }

        @Override
        public int compare(MenuMappingHandler.Match match1, MenuMappingHandler.Match match2) {
            return this.comparator.compare(match1.mapping, match2.mapping);
        }
    }

    private class Match {

        private final RequestMappingInfo mapping;

        private final HandlerMethod handlerMethod;

        public Match(RequestMappingInfo mapping, HandlerMethod handlerMethod) {
            this.mapping = mapping;
            this.handlerMethod = handlerMethod;
        }

        @Override
        public String toString() {
            return this.mapping.toString();
        }
    }

    protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
        return info.getMatchingCondition(request);
    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver stringValueResolver) {
        this.embeddedValueResolver = stringValueResolver;
    }

    private final MappingRegistry mappingRegistry = new MappingRegistry();

    protected void initHandlerMethods() {
        for (String beanName : getCandidateBeanNames()) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                processCandidateBean(beanName);
            }
        }
        //handlerMethodsInitialized(getHandlerMethods());
    }



    protected void processCandidateBean(String beanName) {
        Class<?> beanType = null;
        try {
            beanType = obtainApplicationContext().getType(beanName);
        }
        catch (Throwable ex) {
            // An unresolvable bean type, probably from a lazy bean - let's ignore it.
            if (logger.isTraceEnabled()) {
                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
            }
        }
        if (beanType != null && isHandler(beanType)) {
            detectHandlerMethods(beanName);
        }
    }

    /**
     * Look for handler methods in the specified handler bean.
     * @param handler either a bean name or an actual handler instance
     * @see #getMappingForMethod*/

    protected void detectHandlerMethods(Object handler) {
        Class<?> handlerType = (handler instanceof String ?
                obtainApplicationContext().getType((String) handler) : handler.getClass());

        if (handlerType != null) {
            Class<?> userType = ClassUtils.getUserClass(handlerType);
            Map<Method, RequestMappingInfo> methods = MethodIntrospector.selectMethods(userType,
                    (MethodIntrospector.MetadataLookup<RequestMappingInfo>) method -> {
                        try {
                            return getMappingForMethod(method, userType);
                        }
                        catch (Throwable ex) {
                            throw new IllegalStateException("Invalid mapping on handler class [" +
                                    userType.getName() + "]: " + method, ex);
                        }
                    });
            methods.forEach((method, mapping) -> {
                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
                registerHandlerMethod(handler, invocableMethod, mapping);
            });
        }
    }
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        this.mappingRegistry.register(mapping, handler, method);
    }

    @Nullable
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = createRequestMappingInfo(method);
        if (info != null) {
            RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
            if (typeInfo != null) {
                info = typeInfo.combine(info);
            }
        }
        return info;
    }

    @Nullable
    private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
        RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
        return (requestMapping != null ? createRequestMappingInfo(requestMapping, null) : null);
    }


    protected RequestMappingInfo createRequestMappingInfo(
            RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

        RequestMappingInfo.Builder builder = RequestMappingInfo
                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name());
        if (customCondition != null) {
            builder.customCondition(customCondition);
        }
        return builder.build();
    }

    protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
        if (this.embeddedValueResolver == null) {
            return patterns;
        }
        else {
            String[] resolvedPatterns = new String[patterns.length];
            for (int i = 0; i < patterns.length; i++) {
                resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
            }
            return resolvedPatterns;
        }
    }

    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }

    protected String[] getCandidateBeanNames() {
        return obtainApplicationContext().getBeanNamesForType(Object.class);
    }


    protected HandlerMethod createHandlerMethod(Object handler, Method method) {
        if (handler instanceof String) {
            return new HandlerMethod((String) handler,
                    obtainApplicationContext().getAutowireCapableBeanFactory(), method);
        }
        return new HandlerMethod(handler, method);
    }

    @Nullable
    public HandlerMethodMappingNamingStrategy<RequestMappingInfo> getNamingStrategy() {
        return this.namingStrategy;
    }

    @Nullable
    private HandlerMethodMappingNamingStrategy<RequestMappingInfo> namingStrategy;

    protected Set<String> getDirectPaths(RequestMappingInfo mapping) {
        Set<String> urls = Collections.emptySet();
        for (String path : getMappingPathPatterns(mapping)) {
            if (!getPathMatcher().isPattern(path)) {
                urls = (urls.isEmpty() ? new HashSet<>(1) : urls);
                urls.add(path);
            }
        }
        return urls;
    }
    @SuppressWarnings("deprecation")
    protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
        return info.getPatternValues();
    }

    private PathMatcher pathMatcher = new AntPathMatcher();
    public PathMatcher getPathMatcher() {
        return this.pathMatcher;
    }

    class MappingRegistry {

        private final Map<RequestMappingInfo, MenuMappingHandler.MappingRegistration<RequestMappingInfo>> registry = new HashMap<>();

        private final MultiValueMap<String, RequestMappingInfo> pathLookup = new LinkedMultiValueMap<>();

        private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

        private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

        private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        /**
         * Return all registrations.
         * @since 5.3
         */
        public Map<RequestMappingInfo, MenuMappingHandler.MappingRegistration<RequestMappingInfo>> getRegistrations() {
            return this.registry;
        }

        /**
         * Return matches for the given URL path. Not thread-safe.
         * @see #acquireReadLock()
         */
        @Nullable
        public List<RequestMappingInfo> getMappingsByDirectPath(String urlPath) {
            return this.pathLookup.get(urlPath);
        }

        /**
         * Return handler methods by mapping name. Thread-safe for concurrent use.
         */
        public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) {
            return this.nameLookup.get(mappingName);
        }

        /**
         * Return CORS configuration. Thread-safe for concurrent use.
         */
        @Nullable
        public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
            HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod();
            return this.corsLookup.get(original != null ? original : handlerMethod);
        }

        /**
         * Acquire the read lock when using getMappings and getMappingsByUrl.
         */
        public void acquireReadLock() {
            this.readWriteLock.readLock().lock();
        }

        /**
         * Release the read lock after using getMappings and getMappingsByUrl.
         */
        public void releaseReadLock() {
            this.readWriteLock.readLock().unlock();
        }

        public void register(RequestMappingInfo mapping, Object handler, Method method) {
            this.readWriteLock.writeLock().lock();
            try {
                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
                validateMethodMapping(handlerMethod, mapping);

                Set<String> directPaths = MenuMappingHandler.this.getDirectPaths(mapping);
                for (String path : directPaths) {
                    this.pathLookup.add(path, mapping);
                }

                String name = null;
                if (getNamingStrategy() != null) {
                    name = getNamingStrategy().getName(handlerMethod, mapping);
                    addMappingName(name, handlerMethod);
                }

                this.registry.put(mapping, new MenuMappingHandler.MappingRegistration<>(mapping, handlerMethod, directPaths, name));
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }

        private void validateMethodMapping(HandlerMethod handlerMethod, RequestMappingInfo mapping) {
            MenuMappingHandler.MappingRegistration<RequestMappingInfo> registration = this.registry.get(mapping);
            HandlerMethod existingHandlerMethod = (registration != null ? registration.getHandlerMethod() : null);
            if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
                throw new IllegalStateException(
                        "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
                                handlerMethod + "\nto " + mapping + ": There is already '" +
                                existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
            }
        }

        private void addMappingName(String name, HandlerMethod handlerMethod) {
            List<HandlerMethod> oldList = this.nameLookup.get(name);
            if (oldList == null) {
                oldList = Collections.emptyList();
            }

            for (HandlerMethod current : oldList) {
                if (handlerMethod.equals(current)) {
                    return;
                }
            }

            List<HandlerMethod> newList = new ArrayList<>(oldList.size() + 1);
            newList.addAll(oldList);
            newList.add(handlerMethod);
            this.nameLookup.put(name, newList);
        }

        public void unregister(RequestMappingInfo mapping) {
            this.readWriteLock.writeLock().lock();
            try {
                MenuMappingHandler.MappingRegistration<RequestMappingInfo> registration = this.registry.remove(mapping);
                if (registration == null) {
                    return;
                }

                for (String path : registration.getDirectPaths()) {
                    List<RequestMappingInfo> mappings = this.pathLookup.get(path);
                    if (mappings != null) {
                        mappings.remove(registration.getMapping());
                        if (mappings.isEmpty()) {
                            this.pathLookup.remove(path);
                        }
                    }
                }

                removeMappingName(registration);

                this.corsLookup.remove(registration.getHandlerMethod());
            }
            finally {
                this.readWriteLock.writeLock().unlock();
            }
        }

        private void removeMappingName(MenuMappingHandler.MappingRegistration<RequestMappingInfo> definition) {
            String name = definition.getMappingName();
            if (name == null) {
                return;
            }
            HandlerMethod handlerMethod = definition.getHandlerMethod();
            List<HandlerMethod> oldList = this.nameLookup.get(name);
            if (oldList == null) {
                return;
            }
            if (oldList.size() <= 1) {
                this.nameLookup.remove(name);
                return;
            }
            List<HandlerMethod> newList = new ArrayList<>(oldList.size() - 1);
            for (HandlerMethod current : oldList) {
                if (!current.equals(handlerMethod)) {
                    newList.add(current);
                }
            }
            this.nameLookup.put(name, newList);
        }
    }

    static class MappingRegistration<T> {

        private final T mapping;

        private final HandlerMethod handlerMethod;

        private final Set<String> directPaths;

        @Nullable
        private final String mappingName;

        public MappingRegistration(T mapping, HandlerMethod handlerMethod,
                                   @Nullable Set<String> directPaths, @Nullable String mappingName) {

            Assert.notNull(mapping, "Mapping must not be null");
            Assert.notNull(handlerMethod, "HandlerMethod must not be null");
            this.mapping = mapping;
            this.handlerMethod = handlerMethod;
            this.directPaths = (directPaths != null ? directPaths : Collections.emptySet());
            this.mappingName = mappingName;
        }

        public T getMapping() {
            return this.mapping;
        }

        public HandlerMethod getHandlerMethod() {
            return this.handlerMethod;
        }

        public Set<String> getDirectPaths() {
            return this.directPaths;
        }

        @Nullable
        public String getMappingName() {
            return this.mappingName;
        }
    }

}
